package com.atlassian.jira.plugin.reports.ganttchart;

import com.atlassian.jira.issue.IssueFieldConstants;
import com.atlassian.jira.issue.fields.NavigableField;
import com.atlassian.jira.issue.index.DocumentConstants;
import com.atlassian.jira.issue.search.SearchException;
import com.atlassian.jira.issue.search.SearchProvider;
import com.atlassian.jira.issue.search.SearchRequest;
import com.atlassian.jira.issue.search.SearchSort;
import com.atlassian.jira.issue.search.parameters.lucene.DateParameter;
import com.atlassian.jira.issue.search.parameters.lucene.ProjectParameter;
import com.atlassian.jira.plugin.reports.AbstractJFreeChartReport;
import com.atlassian.jira.project.ProjectManager;
import com.atlassian.jira.util.I18nHelper;
import com.atlassian.jira.util.ParameterUtils;
import com.atlassian.jira.web.action.ProjectActionSupport;
import com.atlassian.jira.web.bean.I18nBean;
import com.atlassian.jira.web.util.OutlookDate;
import com.atlassian.jira.web.util.OutlookDateManager;
import com.atlassian.jira.ComponentManager;
import com.atlassian.jira.config.properties.APKeys;
import com.atlassian.jira.config.properties.ApplicationProperties;
import com.atlassian.core.util.DateUtils;
import com.atlassian.core.util.InvalidDurationException;

import com.atlassian.jira.issue.link.LinkCollection;
import com.atlassian.jira.issue.link.IssueLinkType;

import com.opensymphony.user.User;

import java.awt.Color;
import java.io.IOException;
import java.util.*;

import org.apache.log4j.Logger;
import org.jfree.chart.ChartFactory;
import org.jfree.chart.JFreeChart;
import org.jfree.data.gantt.Task;
import org.jfree.data.gantt.TaskSeries;
import org.jfree.data.gantt.TaskSeriesCollection;
import org.ofbiz.core.entity.EntityOperator;
import org.ofbiz.core.entity.GenericValue;

/**
 * Generate a gantt chart displaying number of issues progress in a specified period by
 * user. The report only displays information on issues with due dates and estimates.
 */
public class GraphicalGanttChartReport extends AbstractJFreeChartReport
{
    private static final Logger log = Logger.getLogger(GraphicalGanttChartReport.class);

    // These are nice and big so that the image does not look bad in larger res browsers
    // but this comes at the price of a bigger filesize. The height is dynamically scaled
    // but the width is left as fixed.

    // The height to extend per issue
    private static final int IMAGE_HEIGHT_PER_ISSUE = 50;
    // The minimum height for the generated image
    private static final int IMAGE_MIN_HEIGHT = 200;
    // The height for the generated image
    private static final int IMAGE_WIDTH = 800;

    private final SearchProvider searchProvider;
    private final OutlookDateManager outlookDateManager;
    private final ProjectManager projectManager;
    private ApplicationProperties applicationProperties;
    private int numIssues = 0;
    private HashMap links = new HashMap();

    public boolean showReport()
    {
        return applicationProperties.getOption(APKeys.JIRA_OPTION_TIMETRACKING);
    }

    public GraphicalGanttChartReport(SearchProvider searchProvider, OutlookDateManager outlookDateManager, ProjectManager projectManager, ApplicationProperties applicationProperties)
    {
        this.searchProvider = searchProvider;
        this.outlookDateManager = outlookDateManager;
        this.projectManager = projectManager;
        this.applicationProperties = applicationProperties;
    }

    /** 
     * This will generate the report html. This method retrieves the parameters inputed by the
     * user and finds the issues that match the criteria. A gantt chart is generated and
     * displayed through the velocity template.
     *
     * @return html containing the report.
     */
    public String generateReportHtml(ProjectActionSupport action, Map params) throws Exception
    {
        User remoteUser = action.getRemoteUser();
        I18nHelper i18nBean = new I18nBean(remoteUser);

        // Retrieve the project parameter
        Long projectId = ParameterUtils.getLongParam(params, "projectid");
        //Long projectId = action.getSelectedProject().getLong("id");
        // Retrieve the start and end dates and the time interval specified by the user
        Date startDate = ParameterUtils.getDateParam(params, "startDate", i18nBean.getLocale());
        Date endDate = ParameterUtils.getDateParam(params, "endDate", i18nBean.getLocale());
        String chartTitle = null;
        String xLabel = action.getText("report.ganttchart.chart.x.axis.label");
        String yLabel = action.getText("report.ganttchart.chart.y.axis.label");

        // Search for the issues as specified by the user input
        Map issuesByUser = getIssuesForProjectInRange(remoteUser, startDate, endDate, projectId);

        // Pass the issues to the velocity template
        Map velocityParams = new HashMap();
	velocityParams.put("links", links);
        velocityParams.put("startDate", startDate);
        velocityParams.put("endDate", endDate);
        velocityParams.put("outlookDate", outlookDateManager.getOutlookDate(i18nBean.getLocale()));

        /* projectManager.getProject() is deprecated, but we don't want to break older installs of jira so we'll wait
           to update to the preferred projectManager.getProjectObj() */
        velocityParams.put("project", /*action.getSelectedProject()*/projectManager.getProject(projectId));
        if(issuesByUser.keySet().size() != 0) {
          velocityParams.put("ganttChart", generateAndSaveGanttChart(getChartData(issuesByUser, action),
                                                                     chartTitle, yLabel, xLabel));
        }
        velocityParams.put("issuesByUser", issuesByUser);
        velocityParams.put("action", action);
        velocityParams.put("report", this);

        return descriptor.getHtml("view", velocityParams);
    }

    /**
     * Retrieve the issues opened during the time period specified.
     *
     * @param remoteUser is the user performing the search.
     * @param startDate is the beginning of the date range of the issues due dates for the report.
     * @param endDate is the end of the date range of the issues due dates for the report.
     * @param projectId is the project to generate the report for.
     *
     * @return a map containing GenericValue objects representing issues keyed by their assigned users.
     */
    public Map getIssuesForProjectInRange(User remoteUser, Date startDate, Date endDate, Long projectId) throws SearchException
    {
        SearchRequest sr = new SearchRequest(remoteUser);
        sr.addParameter(new DateParameter(DocumentConstants.ISSUE_DUEDATE, EntityOperator.GREATER_THAN_EQUAL_TO, startDate));
        sr.addParameter(new DateParameter(DocumentConstants.ISSUE_DUEDATE, EntityOperator.LESS_THAN_EQUAL_TO, endDate));
        sr.addParameter(new ProjectParameter(projectId));
        sr.addSearchSort(new SearchSort(NavigableField.ORDER_ASCENDING, IssueFieldConstants.CREATED));
        sr.addSearchSort(new SearchSort(NavigableField.ORDER_ASCENDING, IssueFieldConstants.ASSIGNEE));

        /* this usage of search is deprecated, but we won't update it yet so we don't break older installs of JIRA */
        return categorizeSearchResults(searchProvider.search(sr, remoteUser), remoteUser);
    }

    /**
     * Validate the parameters set by the user.
     */
    public void validate(ProjectActionSupport action, Map params)
    {
        User remoteUser = action.getRemoteUser();
        I18nHelper i18nBean = new I18nBean(remoteUser);

        Date startDate = ParameterUtils.getDateParam(params, "startDate", i18nBean.getLocale());
        Date endDate = ParameterUtils.getDateParam(params, "endDate", i18nBean.getLocale());
        Long projectId = ParameterUtils.getLongParam(params, "projectid");

        OutlookDate outlookDate = outlookDateManager.getOutlookDate(i18nBean.getLocale());

        if (startDate == null) {
            action.addError("startDate", action.getText("report.ganttchart.startdate.required"));
        }

        if (endDate == null) {
            action.addError("endDate", action.getText("report.ganttchart.enddate.required"));
        }
        /*
        try
        {
            GenericValue project = action.getSelectedProject();
            if (project == null)
            {
                action.addErrorMessage(action.getText("report.ganttchart.projectid.invalid"));
                return;
            }
        }
        catch (Exception e)
        {
            action.addErrorMessage(action.getText("report.ganttchart.projectid.invalid"));
        }
        */
        if (projectId == null) {
            action.addError("projectid", action.getText("report.ganttchart.projectid.invalid"));
        }
        // The end date must be after the start date
        if (startDate != null && endDate != null && endDate.before(startDate)) {
            action.addError("endDate", action.getText("report.ganttchart.before.startdate"));
        }
    }

    /** 
     * Generates a gantt chart from the incomming issueData and stores the image for viewing.
     * @param issueData is a jfreechart collection containing the issues in a form that the
     * charting tool understands.
     * @param chartTitle identifies the title to be used on the generated chart.
     * @param yLabel identifies the label to use on the generated charts y-axis.
     * @param xLabel identifies the label to use on the generated charts x-axis.
     * @return a string containing the filename of the generated chart.
     */
    public String generateAndSaveGanttChart(TaskSeriesCollection issueData, String chartTitle,
                                            String yLabel, String xLabel) {
        JFreeChart ganttChart = 
            ChartFactory.createGanttChart(chartTitle, yLabel, xLabel, issueData, 
                                          true, true, true);
        ganttChart.setBackgroundPaint(Color.WHITE);
        try {
            return saveChartForViewing(ganttChart, IMAGE_WIDTH, getHeight());
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    // remove all the issues that have no estimate, there should be a way to make
    // the search do this by adding a search parameter on the time_estimate field 
    // but I can not see the correct search parameter. This is also sorting issues
    // by user and this could likely be done more efficiently.
    private Map categorizeSearchResults(Collection results, User remoteUser) {
        HashMap issuesByUser = new HashMap();
        numIssues = 0;
        for (Iterator it = results.iterator(); it.hasNext();) {
            GenericValue issue = (GenericValue)it.next();
            if(issue != null && 
               issue.getString(IssueFieldConstants.TIME_ESTIMATE) != null) {
                String assignee = issue.getString(IssueFieldConstants.ASSIGNEE);
                Collection userIssues = (Collection)issuesByUser.get(assignee);
                if(userIssues == null) {
                    userIssues = new ArrayList();
                    issuesByUser.put(assignee, userIssues);
                }

            	// Retrieve a collection of all linked issues and their link types
            	LinkCollection linkCollection = ComponentManager.getInstance().getIssueLinkManager().getLinkCollection(issue, remoteUser);
            	Set linkTypes = linkCollection.getLinkTypes();


		    HashMap data = new HashMap();

                    // For each link type
/*
                    for (Iterator iterator1 = linkTypes.iterator(); iterator1.hasNext();)
                    {
                    	
			IssueLinkType linkType = (IssueLinkType) iterator1.next();
			if (linkType != null) {
                    	    List outwardIssues = linkCollection.getOutwardIssues(linkType.getName());
			    ArrayList outwardLinks = new ArrayList();
			    for (Iterator iterator2 = outwardIssues .iterator(); iterator2.hasNext();)
                            {
			        GenericValue issueOut = (GenericValue)iterator2.next();
			        if (issueOut != null) outwardLinks.add( issueOut );
			    }

                    	    List inwardIssues = linkCollection.getInwardIssues(linkType.getName());
			    ArrayList inwardLinks = new ArrayList();
			    for (Iterator iterator3 = outwardIssues .iterator(); iterator3.hasNext();)
                            {
			        GenericValue issueIn = (GenericValue)iterator3.next();
			        if (issueIn != null) inwardLinks.add( issueIn );
			    }
		    	    data.put(linkType.getName() + " outward", outwardLinks);
		    	    data.put(linkType.getName() + " inward", inwardLinks);
			}
		    } 
		    links.put(issue.getString("id"), data);  
*/

		
		links.put( issue.getString("id"), linkCollection.getAllIssues() );
		    
		
                userIssues.add(issue);
                numIssues++;
            }
        }
        return issuesByUser;
    }

    // Dynamically scale the height based on how many issues we are displaying.
    private int getHeight() {
        int height = IMAGE_HEIGHT_PER_ISSUE * numIssues;
        return (height < IMAGE_MIN_HEIGHT) ? IMAGE_MIN_HEIGHT : height;
    }

    // Translates a collection of issues into a TaskSeriesCollection that jfreechart can
    // generate a chart image from.
    private TaskSeriesCollection getChartData(Map issues, ProjectActionSupport action) {
        TaskSeriesCollection tsc = new TaskSeriesCollection();
        for(Iterator it = issues.keySet().iterator(); it.hasNext();) {
            String assignee = (String)it.next();
            String fullName = action.getUserFullName(assignee);
            TaskSeries tasks = new TaskSeries(fullName);
            Collection userIssues = (Collection)issues.get(assignee);
            for(Iterator iter = userIssues.iterator(); iter.hasNext(); ) {
                GenericValue issue = (GenericValue)iter.next();
                tasks.add(generateTaskFromIssue(issue, action));
            }
            tsc.add(tasks);
        }        
        return tsc;
    }

    // Translates an issue into a jfreechart Task.
    private Task generateTaskFromIssue(GenericValue issue, ProjectActionSupport action) {
        // Calculate the start time of the task as the due date minus the estimated time
        // since the issue does not have a start task date field.
        long timeEstimate = getNullSafeLong(issue, IssueFieldConstants.TIME_ESTIMATE);
        long timeSpent = getNullSafeLong(issue, IssueFieldConstants.TIME_SPENT);
        long totalTimeEstimate = timeEstimate + timeSpent;
        Date start = new Date((issue.getTimestamp(IssueFieldConstants.DUE_DATE).getTime() 
                               - (1000 * totalTimeEstimate)));
        Date end = new Date(issue.getTimestamp(IssueFieldConstants.DUE_DATE).getTime());
        double percentComplete = getPercentComplete(timeSpent, end, start);
        
        StringBuffer label = new StringBuffer(issue.getString(DocumentConstants.ISSUE_KEY));
        label.append(" (");
        label.append((int)(percentComplete * 100));
        label.append("%)");
        
        Task retVal = new Task(label.toString(), start, end);        
        retVal.setPercentComplete(percentComplete);

        return retVal;
    }

    // Calculate the percent complete for an issue.
    private double getPercentComplete(long timeSpent, Date end, Date start) {
        return (timeSpent != 0) ? (1000.0 * (double)timeSpent) /
            ((double)end.getTime() - (double)start.getTime()) : 0.0;
    }

    // Translates a null value to 0.
    private long getNullSafeLong(GenericValue issue, String key) {
        Long value = issue.getLong(key);
        if(value != null) {
            long rawTime = value.longValue();

            String duration = DateUtils.getDurationString(rawTime, 8, 5);

            try
            {
                long realTime = DateUtils.getDuration(duration, 24, 7);
                return realTime;
            }
            catch (InvalidDurationException e)
            {
                e.printStackTrace();
                return 0;
            }
        } else {
            return 0L;
        }        
    }

    private String getNiceTimeDuration(long duration)
    {
        int daysInWeek = new Integer(applicationProperties.getDefaultBackedString(APKeys.JIRA_TIMETRACKING_DAYS_PER_WEEK)).intValue();
        int hoursInDay = new Integer(applicationProperties.getDefaultBackedString(APKeys.JIRA_TIMETRACKING_HOURS_PER_DAY)).intValue();
        return DateUtils.getDurationPretty(duration, hoursInDay, daysInWeek, descriptor.getI18nBean().getDefaultResourceBundle());
    }

    public String getNiceTimeDuration(GenericValue issue, String field)
    {
        Long duration = issue.getLong(field);

        return getNiceTimeDuration(duration);
    }

    private String getNiceTimeDuration(Long duration)
    {
        if (duration == null)
            return "unknown";
        return getNiceTimeDuration(duration.longValue());
    }

    public int getPercentageComplete(GenericValue issue)
    {
        long timeEst = getNullSafeLong(issue, "timeestimate");
        long timeSpent = getNullSafeLong(issue, "timespent");

        double percentage = 100 * ((double)timeSpent) / ((double)timeEst + timeSpent);
        return (int)percentage;
    }

}
